// Copyright (c) 2018-2024 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
//
// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)

#include "vkvg_device_internal.h"
#include "vkvg_context_internal.h"
#include "vkvg_surface_internal.h"
#include "vkvg_pattern.h"
#include "vkh_queue.h"

#ifdef DEBUG
static vec2     debugLinePoints[1000];
static uint32_t dlpCount = 0;
#if defined(VKVG_DBG_UTILS)
const float DBG_LAB_COLOR_SAV[4]  = {1, 0, 1, 1};
const float DBG_LAB_COLOR_CLIP[4] = {0, 1, 1, 1};
#endif
#endif

// todo:this could be used to define a default background
static VkClearValue clearValues[3] = {
    {.color.float32 = {0, 0, 0, 0}}, {.depthStencil = {1.0f, 0}}, {.color.float32 = {0, 0, 0, 0}}};

void _init_ctx(VkvgContext ctx) {
    ctx->lineWidth                       = 1.f;
    ctx->miterLimit                      = 10.f;
    ctx->curOperator                     = VKVG_OPERATOR_OVER;
    ctx->curFillRule                     = VKVG_FILL_RULE_NON_ZERO;
    ctx->bounds                          = (VkRect2D){{0, 0}, {ctx->pSurf->width, ctx->pSurf->height}};
    ctx->pushConsts                      = (push_constants){{.a = 1},
                                                            {(float)ctx->pSurf->width, (float)ctx->pSurf->height},
                                                            VKVG_PATTERN_TYPE_SOLID,
                                                            1.0f,
                                                            VKVG_IDENTITY_MATRIX,
                                                            VKVG_IDENTITY_MATRIX};
    ctx->clearRect                       = (VkClearRect){{{0}, {ctx->pSurf->width, ctx->pSurf->height}}, 0, 1};
    ctx->renderPassBeginInfo.framebuffer = ctx->pSurf->fb;
    ctx->renderPassBeginInfo.renderArea.extent.width  = ctx->pSurf->width;
    ctx->renderPassBeginInfo.renderArea.extent.height = ctx->pSurf->height;
    ctx->renderPassBeginInfo.pClearValues             = clearValues;

    LOCK_SURFACE(ctx->pSurf)

    if (ctx->pSurf->newSurf) {
        ctx->renderPassBeginInfo.renderPass = ctx->dev->renderPass_ClearAll;
        ctx->pSurf->newSurf = false;
    } else {
        ctx->renderPassBeginInfo.renderPass = ctx->dev->renderPass_ClearStencil;
    }

    UNLOCK_SURFACE(ctx->pSurf);

    vkvg_surface_reference(ctx->pSurf);

    ctx->renderPassBeginInfo.clearValueCount = ctx->dev->samples == VK_SAMPLE_COUNT_1_BIT ? 2 : 3;

    ctx->selectedCharSize    = 10 << 6;
    ctx->currentFont         = NULL;
    ctx->selectedFontName[0] = 0;
    ctx->pattern             = NULL;
    ctx->curColor            = 0xff000000; // opaque black
    ctx->cmdStarted          = false;
    ctx->curClipState        = vkvg_clip_state_none;


    ctx->vertCount = ctx->indCount = 0;
#ifdef VKVG_ENABLE_VK_TIMELINE_SEMAPHORE
    ctx->timelineStep = 0;
#endif
}

VkvgContext vkvg_create(VkvgSurface surf) {
    LOG(VKVG_LOG_INFO, "CREATE Context\n");
    if (vkvg_surface_status(surf)) {
        LOG(VKVG_LOG_ERR, "CREATE Context failed, invalid surface\n");
        return (VkvgContext)&_vkvg_status_invalid_surface;
    }
    VkvgDevice  dev = surf->dev;
    if (vkvg_device_status(dev)) {
        LOG(VKVG_LOG_ERR, "CREATE Context failed, invalid device\n");
        return (VkvgContext)&_vkvg_status_device_error;
    }
    VkvgContext ctx = NULL;

    if (_device_try_get_cached_context(dev, &ctx)) {
        ctx->pSurf = surf;
        ctx->status = VKVG_STATUS_SUCCESS;
        _init_ctx(ctx);
        _update_descriptor_set(ctx, surf->dev->emptyImg, ctx->dsSrc);
        _clear_path(ctx);
        ctx->cmd = ctx->cmdBuffers[0]; // current recording buffer
        return ctx;
    }
    ctx = (vkvg_context *)calloc(1, sizeof(vkvg_context));

    if (!ctx) {
        LOG(VKVG_LOG_ERR, "CREATE context failed, no memory\n");
        return (VkvgContext)&_vkvg_status_no_memory;
    }

    LOG(VKVG_LOG_INFO, "CREATE Context: ctx = %p; surf = %p\n", ctx, surf);
    ctx->pSurf = surf;

    ctx->sizePoints   = VKVG_PTS_SIZE;
    ctx->sizeVertices = ctx->sizeVBO = VKVG_VBO_SIZE;
    ctx->sizeIndices = ctx->sizeIBO = VKVG_IBO_SIZE;
    ctx->sizePathes                 = VKVG_PATHES_SIZE;
    ctx->renderPassBeginInfo.sType  = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;

    ctx->dev = surf->dev;

    ctx->points      = (vec2 *)malloc(VKVG_VBO_SIZE * sizeof(vec2));
    ctx->pathes      = (uint32_t *)malloc(VKVG_PATHES_SIZE * sizeof(uint32_t));
    ctx->vertexCache = (Vertex *)malloc(ctx->sizeVertices * sizeof(Vertex));
    ctx->indexCache  = (VKVG_IBO_INDEX_TYPE *)malloc(ctx->sizeIndices * sizeof(VKVG_IBO_INDEX_TYPE));

    if (!ctx->points || !ctx->pathes || !ctx->vertexCache || !ctx->indexCache) {
        if (ctx->points)
            free(ctx->points);
        if (ctx->pathes)
            free(ctx->pathes);
        if (ctx->vertexCache)
            free(ctx->vertexCache);
        if (ctx->indexCache)
            free(ctx->indexCache);
        free(ctx);
        LOG(VKVG_LOG_ERR, "CREATE context failed, no memory\n");
        return (VkvgContext)&_vkvg_status_no_memory;
    }

    _init_ctx(ctx);

    VkhDevice vkhd = (VkhDevice)&dev->vkDev;
    // for context to be thread safe, command pool and descriptor pool have to be created in the thread of the context.
    ctx->cmdPool = vkh_cmd_pool_create(vkhd, dev->gQueue->familyIndex, VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT);

#ifndef VKVG_ENABLE_VK_TIMELINE_SEMAPHORE
    ctx->flushFence = vkh_fence_create_signaled((VkhDevice)&ctx->dev->vkDev);
#endif

    _create_vertices_buff(ctx);
    _create_gradient_buff(ctx);
    _create_cmd_buff(ctx);
    _createDescriptorPool(ctx);
    _init_descriptor_sets(ctx);
    _font_cache_update_context_descset(ctx);
    _update_descriptor_set(ctx, surf->dev->emptyImg, ctx->dsSrc);
    _update_gradient_desc_set(ctx);

    _clear_path(ctx);

    ctx->cmd = ctx->cmdBuffers[0]; // current recording buffer

    ctx->references = 1;
    ctx->status     = VKVG_STATUS_SUCCESS;

    LOG(VKVG_LOG_DBG_ARRAYS, "INIT\tctx = %p; pathes:%ju pts:%ju vch:%d vbo:%d ich:%d ibo:%d\n", ctx,
        (uint64_t)ctx->sizePathes, (uint64_t)ctx->sizePoints, ctx->sizeVertices, ctx->sizeVBO, ctx->sizeIndices,
        ctx->sizeIBO);

#if defined(DEBUG) && defined(VKVG_DBG_UTILS)
    vkh_device_set_object_name(vkhd, VK_OBJECT_TYPE_COMMAND_POOL, (uint64_t)ctx->cmdPool, "CTX Cmd Pool");
    vkh_device_set_object_name(vkhd, VK_OBJECT_TYPE_COMMAND_BUFFER, (uint64_t)ctx->cmdBuffers[0], "CTX Cmd Buff A");
    vkh_device_set_object_name(vkhd, VK_OBJECT_TYPE_COMMAND_BUFFER, (uint64_t)ctx->cmdBuffers[1], "CTX Cmd Buff B");
#ifndef VKVG_ENABLE_VK_TIMELINE_SEMAPHORE
    vkh_device_set_object_name(vkhd, VK_OBJECT_TYPE_FENCE, (uint64_t)ctx->flushFence, "CTX Flush Fence");
#endif
    vkh_device_set_object_name(vkhd, VK_OBJECT_TYPE_DESCRIPTOR_POOL, (uint64_t)ctx->descriptorPool,
                               "CTX Descriptor Pool");
    vkh_device_set_object_name(vkhd, VK_OBJECT_TYPE_DESCRIPTOR_SET, (uint64_t)ctx->dsSrc, "CTX DescSet SOURCE");
    vkh_device_set_object_name(vkhd, VK_OBJECT_TYPE_DESCRIPTOR_SET, (uint64_t)ctx->dsFont, "CTX DescSet FONT");
    vkh_device_set_object_name(vkhd, VK_OBJECT_TYPE_DESCRIPTOR_SET, (uint64_t)ctx->dsGrad, "CTX DescSet GRADIENT");

    vkh_device_set_object_name(vkhd, VK_OBJECT_TYPE_BUFFER, (uint64_t)ctx->indices.buffer, "CTX Index Buff");
    vkh_device_set_object_name(vkhd, VK_OBJECT_TYPE_BUFFER, (uint64_t)ctx->vertices.buffer, "CTX Vertex Buff");
#endif

    return ctx;
}
void vkvg_flush(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;
    _flush_cmd_buff(ctx);
    _wait_ctx_flush_end(ctx);
    /*
    #ifdef DEBUG

        vec4 red = {0,0,1,1};
        vec4 green = {0,1,0,1};
        vec4 white = {1,1,1,1};

        int j = 0;
        while (j < dlpCount) {
            add_line(ctx, debugLinePoints[j], debugLinePoints[j+1],green);
            j+=2;
            add_line(ctx, debugLinePoints[j], debugLinePoints[j+1],red);
            j+=2;
            add_line(ctx, debugLinePoints[j], debugLinePoints[j+1],white);
            j+=2;
        }
        dlpCount = 0;
        CmdBindPipeline(ctx->cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, ctx->pSurf->dev->pipelineLineList);
        CmdDrawIndexed(ctx->cmd, ctx->indCount-ctx->curIndStart, 1, ctx->curIndStart, 0, 1);
        _flush_cmd_buff(ctx);
    #endif
    */
}

void _clear_context(VkvgContext ctx) {
    // free saved context stack elmt
    vkvg_context_save_t *next = ctx->pSavedCtxs;
    ctx->pSavedCtxs           = NULL;
    while (next != NULL) {
        vkvg_context_save_t *cur = next;
        next                     = cur->pNext;
        _free_ctx_save(cur);
    }
    // free additional stencil use in save/restore process
    if (ctx->savedStencils) {
        uint8_t curSaveStencil = ctx->curSavBit / 6;
        for (int i = curSaveStencil; i > 0; i--)
            vkh_image_destroy(ctx->savedStencils[i - 1]);
        free(ctx->savedStencils);
        ctx->savedStencils = NULL;
        ctx->curSavBit     = 0;
    }

    // remove context from double linked list of context in device
    /*if (ctx->dev->lastCtx == ctx){
        ctx->dev->lastCtx = ctx->pPrev;
        if (ctx->pPrev != NULL)
            ctx->pPrev->pNext = NULL;
    }else if (ctx->pPrev == NULL){
        //first elmt, and it's not last one so pnext is not null
        ctx->pNext->pPrev = NULL;
    }else{
        ctx->pPrev->pNext = ctx->pNext;
        ctx->pNext->pPrev = ctx->pPrev;
    }*/
    if (ctx->dashCount > 0) {
        free(ctx->dashes);
        ctx->dashCount = 0;
    }
}

void vkvg_destroy(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;

    ctx->references--;
    if (ctx->references > 0)
        return;

    LOG(VKVG_LOG_INFO, "DESTROY Context: ctx = %p (status:%d); surf = %p\n", ctx, ctx->status, ctx->pSurf);

    vkvg_flush(ctx);

    LOG(VKVG_LOG_DBG_ARRAYS, "END\tctx = %p; pathes:%d pts:%d vch:%d vbo:%d ich:%d ibo:%d\n", ctx, ctx->sizePathes,
        ctx->sizePoints, ctx->sizeVertices, ctx->sizeVBO, ctx->sizeIndices, ctx->sizeIBO);

#if VKVG_RECORDING
    if (ctx->recording)
        _destroy_recording(ctx->recording);
#endif

    if (ctx->pattern)
        vkvg_pattern_destroy(ctx->pattern);

    _clear_context(ctx);

#if VKVG_DBG_STATS
    if (ctx->dev->threadAware)
        mtx_lock(&ctx->dev->mutex);

    vkvg_debug_stats_t *dbgstats = &ctx->dev->debug_stats;
    if (dbgstats->sizePoints < ctx->sizePoints)
        dbgstats->sizePoints = ctx->sizePoints;
    if (dbgstats->sizePathes < ctx->sizePathes)
        dbgstats->sizePathes = ctx->sizePathes;
    if (dbgstats->sizeVertices < ctx->sizeVertices)
        dbgstats->sizeVertices = ctx->sizeVertices;
    if (dbgstats->sizeIndices < ctx->sizeIndices)
        dbgstats->sizeIndices = ctx->sizeIndices;
    if (dbgstats->sizeVBO < ctx->sizeVBO)
        dbgstats->sizeVBO = ctx->sizeVBO;
    if (dbgstats->sizeIBO < ctx->sizeIBO)
        dbgstats->sizeIBO = ctx->sizeIBO;

    if (ctx->dev->threadAware)
        mtx_unlock(&ctx->dev->mutex);
#endif

    vkvg_surface_destroy(ctx->pSurf);

    if (!ctx->status && ctx->dev->cachedContextCount < VKVG_MAX_CACHED_CONTEXT_COUNT) {
        _device_store_context(ctx);
        ctx->status = VKVG_STATUS_IN_CACHE;
        return;
    }

    _release_context_ressources(ctx);

    ctx = NULL;
}
void vkvg_set_opacity(VkvgContext ctx, float opacity) {
    if (vkvg_status(ctx))
        return;

    if (EQUF(ctx->pushConsts.opacity, opacity))
        return;

    _emit_draw_cmd_undrawn_vertices(ctx);
    ctx->pushConsts.opacity = opacity;
    ctx->pushCstDirty       = true;
}
float vkvg_get_opacity(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return 0;
    return ctx->pushConsts.opacity;
}
vkvg_status_t vkvg_status(VkvgContext ctx) { return !ctx ? VKVG_STATUS_NULL_POINTER : ctx->status; }
VkvgContext   vkvg_reference(VkvgContext ctx) {
    if (!ctx->status)
        ctx->references++;
    return ctx;
}
uint32_t vkvg_get_reference_count(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return 0;
    return ctx->references;
}
void vkvg_new_sub_path(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;

    RECORD(ctx, VKVG_CMD_NEW_SUB_PATH);
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: new_sub_path:\n");

    _finish_path(ctx);
}
void vkvg_new_path(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;

    RECORD(ctx, VKVG_CMD_NEW_PATH);
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: new_path:\n");

    _clear_path(ctx);
}
void vkvg_close_path(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;

    RECORD(ctx, VKVG_CMD_CLOSE_PATH);
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: close_path:\n");

    if (ctx->pathes[ctx->pathPtr] & PATH_CLOSED_BIT) // already closed
        return;
    // check if at least 3 points are present
    if (ctx->pathes[ctx->pathPtr] < 3)
        return;

    // prevent closing on the same point
    if (vec2_equ(ctx->points[ctx->pointCount - 1], ctx->points[ctx->pointCount - ctx->pathes[ctx->pathPtr]])) {
        if (ctx->pathes[ctx->pathPtr] < 4) // ensure enough points left for closing
            return;
        _remove_last_point(ctx);
    }

    ctx->pathes[ctx->pathPtr] |= PATH_CLOSED_BIT;

    _finish_path(ctx);
}
void vkvg_rel_line_to(VkvgContext ctx, float dx, float dy) {
    if (vkvg_status(ctx))
        return;

    RECORD(ctx, VKVG_CMD_REL_LINE_TO, dx, dy);
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: rel_line_to: %f, %f\n", dx, dy);

    if (_current_path_is_empty(ctx))
        _add_point(ctx, 0, 0);
    vec2 cp = _get_current_position(ctx);
    _line_to(ctx, cp.x + dx, cp.y + dy);
}
void vkvg_line_to(VkvgContext ctx, float x, float y) {
    if (vkvg_status(ctx))
        return;

    RECORD(ctx, VKVG_CMD_LINE_TO, x, y);
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: line_to: %f, %f\n", x, y);
    _line_to(ctx, x, y);
}
void vkvg_arc(VkvgContext ctx, float xc, float yc, float radius, float a1, float a2) {
    if (vkvg_status(ctx))
        return;

    RECORD(ctx, VKVG_CMD_ARC, xc, yc, radius, a1, a2);
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: arc: %f,%f %f %f %f\n", xc, yc, radius, a1, a2);

    while (a2 < a1) // positive arc must have a1<a2
        a2 += 2.f * M_PIF;

    if (a2 - a1 > 2.f * M_PIF) // limit arc to 2PI
        a2 = a1 + 2.f * M_PIF;

    vec2 v = {cosf(a1) * radius + xc, sinf(a1) * radius + yc};

    float step = _get_arc_step(ctx, radius);
    float a    = a1;

    if (_current_path_is_empty(ctx)) {
        _set_curve_start(ctx);
        _add_point(ctx, v.x, v.y);
        if (!ctx->pathPtr)
            ctx->simpleConvex = true;
        else
            ctx->simpleConvex = false;
    } else {
        _line_to(ctx, v.x, v.y);
        _set_curve_start(ctx);
        ctx->simpleConvex = false;
    }

    a += step;

    if (EQUF(a2, a1))
        return;

    while (a < a2) {
        v.x = cosf(a) * radius + xc;
        v.y = sinf(a) * radius + yc;
        _add_point(ctx, v.x, v.y);
        a += step;
    }

    if (EQUF(a2 - a1, M_PIF * 2.f)) { // if arc is complete circle, last point is the same as the first one
        _set_curve_end(ctx);
        vkvg_close_path(ctx);
        return;
    }
    a = a2;
    // vec2 lastP = v;
    v.x = cosf(a) * radius + xc;
    v.y = sinf(a) * radius + yc;
    // if (!vec2_equ (v,lastP))//this test should not be required
    _add_point(ctx, v.x, v.y);
    _set_curve_end(ctx);
}
void vkvg_arc_negative(VkvgContext ctx, float xc, float yc, float radius, float a1, float a2) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_ARC_NEG, xc, yc, radius, a1, a2);
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: %f,%f %f %f %f\n", xc, yc, radius, a1, a2);
    while (a2 > a1)
        a2 -= 2.f * M_PIF;
    if (a1 - a2 > a1 + 2.f * M_PIF) // limit arc to 2PI
        a2 = a1 - 2.f * M_PIF;

    vec2 v = {cosf(a1) * radius + xc, sinf(a1) * radius + yc};

    float step = _get_arc_step(ctx, radius);
    float a    = a1;

    if (_current_path_is_empty(ctx)) {
        _set_curve_start(ctx);
        _add_point(ctx, v.x, v.y);
        if (!ctx->pathPtr)
            ctx->simpleConvex = true;
        else
            ctx->simpleConvex = false;
    } else {
        _line_to(ctx, v.x, v.y);
        _set_curve_start(ctx);
        ctx->simpleConvex = false;
    }

    a -= step;

    if (EQUF(a2, a1))
        return;

    while (a > a2) {
        v.x = cosf(a) * radius + xc;
        v.y = sinf(a) * radius + yc;
        _add_point(ctx, v.x, v.y);
        a -= step;
    }

    if (EQUF(a1 - a2, M_PIF * 2.f)) { // if arc is complete circle, last point is the same as the first one
        _set_curve_end(ctx);
        vkvg_close_path(ctx);
        return;
    }

    a = a2;
    // vec2 lastP = v;
    v.x = cosf(a) * radius + xc;
    v.y = sinf(a) * radius + yc;
    // if (!vec2_equ (v,lastP))
    _add_point(ctx, v.x, v.y);
    _set_curve_end(ctx);
}
void vkvg_rel_move_to(VkvgContext ctx, float x, float y) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_REL_MOVE_TO, x, y);
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: rel_mote_to: %f, %f\n", x, y);
    if (_current_path_is_empty(ctx))
        _add_point(ctx, 0, 0);
    vec2 cp = _get_current_position(ctx);
    _finish_path(ctx);
    _add_point(ctx, cp.x + x, cp.y + y);
}
void vkvg_move_to(VkvgContext ctx, float x, float y) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_MOVE_TO, x, y);
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: move_to: %f,%f\n", x, y);
    _finish_path(ctx);
    _add_point(ctx, x, y);
}
bool vkvg_has_current_point(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return false;
    return !_current_path_is_empty(ctx);
}
void vkvg_get_current_point(VkvgContext ctx, float *x, float *y) {
    if (vkvg_status(ctx))
        return;
    assert(x);
    assert(y);
    if (_current_path_is_empty(ctx)) {
        *x = *y = 0;
        return;
    }
    vec2 cp = _get_current_position(ctx);
    *x      = cp.x;
    *y      = cp.y;
}
void _curve_to(VkvgContext ctx, float x1, float y1, float x2, float y2, float x3, float y3) {
    // prevent running _recursive_bezier when all 4 curve points are equal
    if (EQUF(x1, x2) && EQUF(x2, x3) && EQUF(y1, y2) && EQUF(y2, y3)) {
        if (_current_path_is_empty(ctx) ||
            (EQUF(_get_current_position(ctx).x, x1) && EQUF(_get_current_position(ctx).y, y1)))
            return;
    }
    ctx->simpleConvex = false;
    _set_curve_start(ctx);
    if (_current_path_is_empty(ctx))
        _add_point(ctx, x1, y1);

    vec2 cp = _get_current_position(ctx);

    // compute dyn distanceTolerance depending on current scale
    float sx = 1, sy = 1;
    vkvg_matrix_get_scale(&ctx->pushConsts.mat, &sx, &sy);
    float distanceTolerance = fabs(0.25f / fmaxf(sx, sy));

    _recursive_bezier(ctx, distanceTolerance, cp.x, cp.y, x1, y1, x2, y2, x3, y3, 0);
    /*cp.x = x3;
    cp.y = y3;
    if (!vec2_equ(ctx->points[ctx->pointCount-1],cp))*/
    _add_point(ctx, x3, y3);
    _set_curve_end(ctx);
}
const double quadraticFact = 2.0 / 3.0;
void _quadratic_to(VkvgContext ctx, float x1, float y1, float x2, float y2) {
    float x0, y0;
    if (_current_path_is_empty(ctx)) {
        x0 = x1;
        y0 = y1;
    } else
        vkvg_get_current_point(ctx, &x0, &y0);
    _curve_to(ctx, x0 + (x1 - x0) * quadraticFact, y0 + (y1 - y0) * quadraticFact, x2 + (x1 - x2) * quadraticFact,
                      y2 + (y1 - y2) * quadraticFact, x2, y2);
}
void vkvg_quadratic_to(VkvgContext ctx, float x1, float y1, float x2, float y2) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_QUADRATIC_TO, x1, y1, x2, y2);
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: quadratic_to: %f, %f, %f, %f\n", x1, y1, x2, y2);
    _quadratic_to(ctx, x1, y1, x2, y2);
}
void vkvg_rel_quadratic_to(VkvgContext ctx, float x1, float y1, float x2, float y2) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_REL_QUADRATIC_TO, x1, y1, x2, y2);
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: rel_quadratic_to: %f, %f, %f, %f\n", x1, y1, x2, y2);
    vec2 cp = _get_current_position(ctx);
    _quadratic_to(ctx, cp.x + x1, cp.y + y1, cp.x + x2, cp.y + y2);
}
void vkvg_curve_to(VkvgContext ctx, float x1, float y1, float x2, float y2, float x3, float y3) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_CURVE_TO, x1, y1, x2, y2, x3, y3);
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: curve_to %f,%f %f,%f %f,%f:\n", x1, y1, x2, y2, x3, y3);
    _curve_to(ctx, x1, y1, x2, y2, x3, y3);
}
void vkvg_rel_curve_to(VkvgContext ctx, float x1, float y1, float x2, float y2, float x3, float y3) {
    if (vkvg_status(ctx))
        return;
    if (_current_path_is_empty(ctx)) {
        ctx->status = VKVG_STATUS_NO_CURRENT_POINT;
        return;
    }
    RECORD(ctx, (uint32_t)VKVG_CMD_REL_CURVE_TO, x1, y1, x2, y2, x3, y3);
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: rel curve_to %f,%f %f,%f %f,%f:\n", x1, y1, x2, y2, x3, y3);
    vec2 cp = _get_current_position(ctx);
    _curve_to(ctx, cp.x + x1, cp.y + y1, cp.x + x2, cp.y + y2, cp.x + x3, cp.y + y3);
}
void vkvg_fill_rectangle(VkvgContext ctx, float x, float y, float w, float h) {
    if (vkvg_status(ctx))
        return;
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: fill_rectangle:\n");
    _vao_add_rectangle(ctx, x, y, w, h);
    //_record_draw_cmd(ctx);
}

vkvg_status_t vkvg_rectangle(VkvgContext ctx, float x, float y, float w, float h) {
    if (vkvg_status(ctx))
        return ctx->status;
    RECORD2(ctx, VKVG_CMD_RECTANGLE, x, y, w, h);
    LOG(VKVG_LOG_INFO_CMD, "\tCMD: rectangle: %f,%f,%f,%f\n", x, y, w, h);
    _finish_path(ctx);

    if (w <= 0 || h <= 0)
        return VKVG_STATUS_INVALID_RECT;

    _add_point(ctx, x, y);
    _add_point(ctx, x + w, y);
    _add_point(ctx, x + w, y + h);
    _add_point(ctx, x, y + h);

    ctx->pathes[ctx->pathPtr] |= (PATH_CLOSED_BIT | PATH_IS_CONVEX_BIT);

    _finish_path(ctx);
    return VKVG_STATUS_SUCCESS;
}
vkvg_status_t vkvg_rounded_rectangle(VkvgContext ctx, float x, float y, float w, float h, float radius) {
    if (vkvg_status(ctx))
        return ctx->status;
    LOG(VKVG_LOG_INFO_CMD, "CMD: rounded_rectangle:\n");
    _finish_path(ctx);

    if (w <= 0 || h <= 0)
        return VKVG_STATUS_INVALID_RECT;

    if ((radius > w / 2.0f) || (radius > h / 2.0f))
        radius = fmin(w / 2.0f, h / 2.0f);

    vkvg_move_to(ctx, x, y + radius);
    vkvg_arc(ctx, x + radius, y + radius, radius, M_PIF, -M_PIF_2);
    vkvg_line_to(ctx, x + w - radius, y);
    vkvg_arc(ctx, x + w - radius, y + radius, radius, -M_PIF_2, 0);
    vkvg_line_to(ctx, x + w, y + h - radius);
    vkvg_arc(ctx, x + w - radius, y + h - radius, radius, 0, M_PIF_2);
    vkvg_line_to(ctx, x + radius, y + h);
    vkvg_arc(ctx, x + radius, y + h - radius, radius, M_PIF_2, M_PIF);
    vkvg_line_to(ctx, x, y + radius);
    vkvg_close_path(ctx);

    return VKVG_STATUS_SUCCESS;
}
void vkvg_rounded_rectangle2(VkvgContext ctx, float x, float y, float w, float h, float rx, float ry) {
    if (vkvg_status(ctx))
        return;
    LOG(VKVG_LOG_INFO_CMD, "CMD: rounded_rectangle2:\n");
    vkvg_move_to(ctx, x + rx, y);
    vkvg_line_to(ctx, x + w - rx, y);
    vkvg_elliptic_arc_to(ctx, x + w, y + ry, false, true, rx, ry, 0);

    vkvg_line_to(ctx, x + w, y + h - ry);
    vkvg_elliptic_arc_to(ctx, x + w - rx, y + h, false, true, rx, ry, 0);

    vkvg_line_to(ctx, x + rx, y + h);
    vkvg_elliptic_arc_to(ctx, x, y + h - ry, false, true, rx, ry, 0);

    vkvg_line_to(ctx, x, y + ry);
    vkvg_elliptic_arc_to(ctx, x + rx, y, false, true, rx, ry, 0);

    vkvg_close_path(ctx);
}
void vkvg_path_extents(VkvgContext ctx, float* const x1, float *const y1, float *const x2, float *const y2) {
    if (vkvg_status(ctx))
        return;

    _finish_path(ctx);

    if (!ctx->pathPtr) { // no path
        *x1 = *x2 = *y1 = *y2 = 0;
        return;
    }

    _vkvg_path_extents(ctx, false, x1, y1, x2, y2);
}

vkvg_clip_state_t _get_previous_clip_state(VkvgContext ctx) {
    if (!ctx->pSavedCtxs) // no clip saved => clear
        return vkvg_clip_state_clear;
    return ctx->pSavedCtxs->clippingState;
}
static const VkClearAttachment clearStencil     = {VK_IMAGE_ASPECT_STENCIL_BIT, 1, {{{0}}}};
static const VkClearAttachment clearColorAttach = {VK_IMAGE_ASPECT_COLOR_BIT, 0, {{{0}}}};

void _reset_clip(VkvgContext ctx) {
    _emit_draw_cmd_undrawn_vertices(ctx);
    if (!ctx->cmdStarted) {
        // if command buffer is not already started and in a renderpass, we use the renderpass
        // with the loadop clear for stencil
        ctx->renderPassBeginInfo.renderPass = ctx->dev->renderPass_ClearStencil;
        // force run of one renderpass (even empty) to perform clear load op
        _start_cmd_for_render_pass(ctx);
        return;
    }
    vkCmdClearAttachments(ctx->cmd, 1, &clearStencil, 1, &ctx->clearRect);
}

void vkvg_reset_clip(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;

    RECORD(ctx, VKVG_CMD_RESET_CLIP);

    if (ctx->curClipState == vkvg_clip_state_clear)
        return;
    if (_get_previous_clip_state(ctx) == vkvg_clip_state_clear)
        ctx->curClipState = vkvg_clip_state_none;
    else
        ctx->curClipState = vkvg_clip_state_clear;

    _reset_clip(ctx);
}
void vkvg_clear(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;

    RECORD(ctx, VKVG_CMD_CLEAR);

    if (_get_previous_clip_state(ctx) == vkvg_clip_state_clear)
        ctx->curClipState = vkvg_clip_state_none;
    else
        ctx->curClipState = vkvg_clip_state_clear;

    _emit_draw_cmd_undrawn_vertices(ctx);
    if (!ctx->cmdStarted) {
        ctx->renderPassBeginInfo.renderPass = ctx->dev->renderPass_ClearAll;
        _start_cmd_for_render_pass(ctx);
        return;
    }
    VkClearAttachment ca[2] = {clearColorAttach, clearStencil};
    vkCmdClearAttachments(ctx->cmd, 2, ca, 1, &ctx->clearRect);
}
void _clip_preserve(VkvgContext ctx) {
    _finish_path(ctx);

    if (!ctx->pathPtr) // nothing to clip
        return;

    _emit_draw_cmd_undrawn_vertices(ctx);

    LOG(VKVG_LOG_INFO, "CLIP: ctx = %p; path cpt = %d;\n", ctx, ctx->pathPtr / 2);

    _ensure_renderpass_is_started(ctx);

#if defined(DEBUG) && defined(VKVG_DBG_UTILS)
    vkh_cmd_label_start(ctx->cmd, "clip", DBG_LAB_COLOR_CLIP);
#endif

    if (ctx->curFillRule == VKVG_FILL_RULE_EVEN_ODD) {
        _poly_fill(ctx, NULL);
        CmdBindPipeline(ctx->cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, ctx->dev->pipelineClipping);
    } else {
        CmdBindPipeline(ctx->cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, ctx->dev->pipelineClipping);
        CmdSetStencilReference(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_FILL_BIT);
        CmdSetStencilCompareMask(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_CLIP_BIT);
        CmdSetStencilWriteMask(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_FILL_BIT);
        _fill_non_zero(ctx);
        _emit_draw_cmd_undrawn_vertices(ctx);
    }
    CmdSetStencilReference(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_CLIP_BIT);
    CmdSetStencilCompareMask(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_FILL_BIT);
    CmdSetStencilWriteMask(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_ALL_BIT);

    _draw_full_screen_quad(ctx, NULL);

    _bind_draw_pipeline(ctx);
    CmdSetStencilCompareMask(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_CLIP_BIT);

#if defined(DEBUG) && defined(VKVG_DBG_UTILS)
    vkh_cmd_label_end(ctx->cmd);
#endif

    ctx->curClipState = vkvg_clip_state_clip;
}
void _fill_preserve(VkvgContext ctx) {
    _finish_path(ctx);

    if (!ctx->pathPtr) // nothing to fill
        return;

    LOG(VKVG_LOG_INFO, "FILL: ctx = %p; path cpt = %d;\n", ctx, ctx->subpathCount);

    if (ctx->curFillRule == VKVG_FILL_RULE_EVEN_ODD) {
        _emit_draw_cmd_undrawn_vertices(ctx);
        vec4 bounds = {FLT_MAX, FLT_MAX, FLT_MIN, FLT_MIN};
        _poly_fill(ctx, &bounds);
        _bind_draw_pipeline(ctx);
        CmdSetStencilCompareMask(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_FILL_BIT);
        _draw_full_screen_quad(ctx, &bounds);
        CmdSetStencilCompareMask(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_CLIP_BIT);
        return;
    }

    if (ctx->vertCount - ctx->curVertOffset + ctx->pointCount > VKVG_IBO_MAX)
        _emit_draw_cmd_undrawn_vertices(ctx); // limit draw call to addressable vx with choosen index type

    if (ctx->pattern) // if not solid color, source img or gradient has to be bound
        _ensure_renderpass_is_started(ctx);
    _fill_non_zero(ctx);
}
void _stroke_preserve(VkvgContext ctx) {
    _finish_path(ctx);

    if (!ctx->pathPtr) // nothing to stroke
        return;

    LOG(VKVG_LOG_INFO, "STROKE: ctx = %p; path ptr = %d;\n", ctx, ctx->pathPtr);

    stroke_context_t str = {0};
    str.hw               = ctx->lineWidth * 0.5f;
    str.lhMax            = ctx->miterLimit * ctx->lineWidth;
    uint32_t ptrPath     = 0;

    while (ptrPath < ctx->pathPtr) {
        uint32_t ptrSegment = 0, lastSegmentPointIdx = 0;
        uint32_t firstPathPointIdx = str.cp;
        uint32_t pathPointCount    = ctx->pathes[ptrPath] & PATH_ELT_MASK;
        uint32_t lastPathPointIdx  = str.cp + pathPointCount - 1;

        dash_context_t dc = {0};

        if (_path_has_curves(ctx, ptrPath)) {
            ptrSegment          = 1;
            lastSegmentPointIdx = str.cp + (ctx->pathes[ptrPath + ptrSegment] & PATH_ELT_MASK) - 1;
        }

        str.firstIdx = (VKVG_IBO_INDEX_TYPE)(ctx->vertCount - ctx->curVertOffset);

        // LOG(VKVG_LOG_INFO_PATH, "\tPATH: points count=%10d end point idx=%10d", ctx->pathes[ptrPath]&PATH_ELT_MASK,
        // lastPathPointIdx);

        if (ctx->dashCount > 0) {
            // init dash stroke
            dc.dashOn        = true;
            dc.curDash       = 0; // current dash index
            dc.totDashLength = 0; // limit offset to total length of dashes
            for (uint32_t i = 0; i < ctx->dashCount; i++)
                dc.totDashLength += ctx->dashes[i];
            if (dc.totDashLength == 0) {
                ctx->status = VKVG_STATUS_INVALID_DASH;
                return;
            }
            dc.curDashOffset = fmodf(
                ctx->dashOffset,
                dc.totDashLength); // cur dash offset between defined path point and last dash segment(on/off) start
            str.iL = lastPathPointIdx;
        } else if (_path_is_closed(ctx, ptrPath)) {
            str.iL = lastPathPointIdx;
        } else {
            _draw_stoke_cap(ctx, &str, ctx->points[str.cp],
                            vec2_line_norm(ctx->points[str.cp], ctx->points[str.cp + 1]), true);
            str.iL = str.cp++;
        }

        if (_path_has_curves(ctx, ptrPath)) {
            while (str.cp < lastPathPointIdx) {

                bool curved = ctx->pathes[ptrPath + ptrSegment] & PATH_HAS_CURVES_BIT;
                if (lastSegmentPointIdx == lastPathPointIdx) // last segment of path, dont draw end point here
                    lastSegmentPointIdx--;
                while (str.cp <= lastSegmentPointIdx)
                    _draw_segment(ctx, &str, &dc, curved);

                ptrSegment++;
                uint32_t cptSegPts  = ctx->pathes[ptrPath + ptrSegment] & PATH_ELT_MASK;
                lastSegmentPointIdx = str.cp + cptSegPts - 1;
                if (lastSegmentPointIdx == lastPathPointIdx && cptSegPts == 1) {
                    // single point last segment
                    ptrSegment++;
                    break;
                }
            }
        } else
            while (str.cp < lastPathPointIdx)
                _draw_segment(ctx, &str, &dc, false);

        if (ctx->dashCount > 0) {
            if (_path_is_closed(ctx, ptrPath)) {
                str.iR = firstPathPointIdx;

                _draw_dashed_segment(ctx, &str, &dc, false);

                str.iL++;
                str.cp++;
            }
            if (!dc.dashOn) {
                // finishing last dash that is already started, draw end caps but not too close to start
                // the default gap is the next void
                int32_t prevDash = (int32_t)dc.curDash - 1;
                if (prevDash < 0)
                    dc.curDash = ctx->dashCount - 1;
                float m = fminf(ctx->dashes[prevDash] - dc.curDashOffset, ctx->dashes[dc.curDash]);
                vec2  p = vec2_sub(ctx->points[str.iR], vec2_mult_s(dc.normal, m));
                _draw_stoke_cap(ctx, &str, p, dc.normal, false);
            }
        } else if (_path_is_closed(ctx, ptrPath)) {
            str.iR       = firstPathPointIdx;
            bool inverse = _build_vb_step(ctx, &str, false);

            VKVG_IBO_INDEX_TYPE *inds = &ctx->indexCache[ctx->indCount - 6];
            VKVG_IBO_INDEX_TYPE  ii   = str.firstIdx;
            if (inverse) {
                inds[1] = ii + 1;
                inds[4] = ii + 1;
                inds[5] = ii;
            } else {
                inds[1] = ii;
                inds[4] = ii;
                inds[5] = ii + 1;
            }
            str.cp++;
        } else
            _draw_stoke_cap(ctx, &str, ctx->points[str.cp],
                            vec2_line_norm(ctx->points[str.cp - 1], ctx->points[str.cp]), false);

        str.cp = firstPathPointIdx + pathPointCount;

        if (ptrSegment > 0)
            ptrPath += ptrSegment;
        else
            ptrPath++;

        // limit batch size here to 1/3 of the ibo index type ability
        if (ctx->vertCount - ctx->curVertOffset > VKVG_IBO_MAX / 3)
            _emit_draw_cmd_undrawn_vertices(ctx);
    }
}

void vkvg_clip(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_CLIP);
    _clip_preserve(ctx);
    _clear_path(ctx);
}
void vkvg_stroke(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_STROKE);
    _stroke_preserve(ctx);
    _clear_path(ctx);
}
void vkvg_fill(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_FILL);
    _fill_preserve(ctx);
    _clear_path(ctx);
}
void vkvg_clip_preserve(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_CLIP_PRESERVE);
    _clip_preserve(ctx);
}
void vkvg_fill_preserve(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_FILL_PRESERVE);
    _fill_preserve(ctx);
}
void vkvg_stroke_preserve(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_STROKE_PRESERVE);
    _stroke_preserve(ctx);
}

void vkvg_paint(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_PAINT);
    _finish_path(ctx);

    if (ctx->pathPtr) {
        vkvg_fill(ctx);
        return;
    }

    _ensure_renderpass_is_started(ctx);
    _draw_full_screen_quad(ctx, NULL);
}
void vkvg_set_source_color(VkvgContext ctx, uint32_t c) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SET_SOURCE_COLOR, c);
    ctx->curColor = c;
    _update_cur_pattern(ctx, NULL);
}
void vkvg_set_source_rgb(VkvgContext ctx, float r, float g, float b) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SET_SOURCE_RGB, r, g, b);
    ctx->curColor = CreateRgbaf(r, g, b, 1);
    _update_cur_pattern(ctx, NULL);
}
void vkvg_set_source_rgba(VkvgContext ctx, float r, float g, float b, float a) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SET_SOURCE_RGBA, r, g, b, a);
    ctx->curColor = CreateRgbaf(r, g, b, a);
    _update_cur_pattern(ctx, NULL);
}
void vkvg_set_source_surface(VkvgContext ctx, VkvgSurface surf, float x, float y) {
    if (vkvg_status(ctx) || vkvg_surface_status(surf))
        return;
    RECORD(ctx, VKVG_CMD_SET_SOURCE_SURFACE, x, y, surf);
    ctx->pushConsts.source.x = x;
    ctx->pushConsts.source.y = y;
    _update_cur_pattern(ctx, vkvg_pattern_create_for_surface(surf));
    ctx->pushCstDirty = true;
}
void vkvg_set_source(VkvgContext ctx, VkvgPattern pat) {
    if (vkvg_status(ctx) || vkvg_pattern_status(pat))
        return;
    RECORD(ctx, VKVG_CMD_SET_SOURCE, pat);
    _update_cur_pattern(ctx, pat);
    vkvg_pattern_reference(pat);
}
void vkvg_set_line_width(VkvgContext ctx, float width) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SET_LINE_WIDTH, width);
    ctx->lineWidth = width;
}
void vkvg_set_miter_limit(VkvgContext ctx, float limit) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SET_LINE_WIDTH, limit);
    ctx->miterLimit = limit;
}
void vkvg_set_line_cap(VkvgContext ctx, vkvg_line_cap_t cap) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SET_LINE_CAP, cap);
    ctx->lineCap = cap;
}
void vkvg_set_line_join(VkvgContext ctx, vkvg_line_join_t join) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SET_LINE_JOIN, join);
    ctx->lineJoin = join;
}
void vkvg_set_operator(VkvgContext ctx, vkvg_operator_t op) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SET_OPERATOR, op);
    if (op == ctx->curOperator)
        return;

    _emit_draw_cmd_undrawn_vertices(
        ctx); // draw call with different ops cant be combined, so emit draw cmd for previous vertices.

    ctx->curOperator = op;

    if (ctx->cmdStarted)
        _bind_draw_pipeline(ctx);
}
void vkvg_set_fill_rule(VkvgContext ctx, vkvg_fill_rule_t fr) {
    if (vkvg_status(ctx))
        return;
#ifndef __APPLE__
    RECORD(ctx, VKVG_CMD_SET_FILL_RULE, fr);
    ctx->curFillRule = fr;
#endif
}
vkvg_fill_rule_t vkvg_get_fill_rule(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return VKVG_FILL_RULE_NON_ZERO;
    return ctx->curFillRule;
}
float vkvg_get_line_width(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return 0;
    return ctx->lineWidth;
}
float vkvg_get_miter_limit(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return 0;
    return ctx->miterLimit;
}
void vkvg_set_dash(VkvgContext ctx, const float *dashes, uint32_t num_dashes, float offset) {
    if (vkvg_status(ctx))
        return;
    if (ctx->dashCount > 0)
        free(ctx->dashes);
    RECORD(ctx, VKVG_CMD_SET_DASH, num_dashes, offset, dashes);
    ctx->dashCount  = num_dashes;
    ctx->dashOffset = offset;
    if (ctx->dashCount == 0)
        return;
    ctx->dashes = (float *)malloc(sizeof(float) * ctx->dashCount);
    memcpy(ctx->dashes, dashes, sizeof(float) * ctx->dashCount);
}
void vkvg_get_dash(VkvgContext ctx, const float *dashes, uint32_t *num_dashes, float *offset) {
    if (vkvg_status(ctx))
        return;
    *num_dashes = ctx->dashCount;
    *offset     = ctx->dashOffset;
    if (ctx->dashCount == 0 || dashes == NULL)
        return;
    memcpy((float *)dashes, ctx->dashes, sizeof(float) * ctx->dashCount);
}

vkvg_line_cap_t vkvg_get_line_cap(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return (vkvg_line_cap_t)0;
    return ctx->lineCap;
}
vkvg_line_join_t vkvg_get_line_join(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return (vkvg_line_join_t)0;
    return ctx->lineJoin;
}
vkvg_operator_t vkvg_get_operator(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return VKVG_OPERATOR_OVER;
    return ctx->curOperator;
}
VkvgPattern vkvg_get_source(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return NULL;
    vkvg_pattern_reference(ctx->pattern);
    return ctx->pattern;
}

void vkvg_select_font_face(VkvgContext ctx, const char *name) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SET_FONT_FACE, name);
    _select_font_face(ctx, name);
}
void vkvg_load_font_from_path(VkvgContext ctx, const char *path, const char *name) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SET_FONT_PATH, name);
    _vkvg_font_identity_t *fid = _font_cache_add_font_identity(ctx, path, name);
    if (!_font_cache_load_font_file_in_memory(fid)) {
        ctx->status = VKVG_STATUS_FILE_NOT_FOUND;
        return;
    }
    _select_font_face(ctx, name);
}
void vkvg_load_font_from_memory(VkvgContext ctx, unsigned char *fontBuffer, long fontBufferByteSize, const char *name) {
    if (vkvg_status(ctx))
        return;
    // RECORD(ctx, VKVG_CMD_SET_FONT_PATH, name);
    _vkvg_font_identity_t *fid = _font_cache_add_font_identity(ctx, NULL, name);
    fid->fontBuffer            = fontBuffer;
    fid->fontBufSize           = fontBufferByteSize;

    _select_font_face(ctx, name);
}
void vkvg_set_font_size(VkvgContext ctx, uint32_t size) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SET_FONT_SIZE, size);
#ifdef VKVG_USE_FREETYPE
    long newSize = size << 6;
#else
    long newSize = size;
#endif
    if (ctx->selectedCharSize == newSize)
        return;
    ctx->selectedCharSize = newSize;
    ctx->currentFont      = NULL;
    ctx->currentFontSize  = NULL;
}

void vkvg_set_text_direction(vkvg_context *ctx, vkvg_direction_t direction) {}

void vkvg_show_text(VkvgContext ctx, const char *text) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SHOW_TEXT, text);
    LOG(VKVG_LOG_INFO_CMD, "CMD: show_text:\n");
    //_ensure_renderpass_is_started(ctx);
    _font_cache_show_text(ctx, text);
    //_flush_undrawn_vertices (ctx);
}

VkvgText vkvg_text_run_create(VkvgContext ctx, const char *text) {
    if (vkvg_status(ctx))
        return NULL;
    VkvgText tr = (vkvg_text_run_t *)calloc(1, sizeof(vkvg_text_run_t));
    _font_cache_create_text_run(ctx, text, -1, tr);
    return tr;
}
VkvgText vkvg_text_run_create_with_length(VkvgContext ctx, const char *text, uint32_t length) {
    if (vkvg_status(ctx))
        return NULL;
    VkvgText tr = (vkvg_text_run_t *)calloc(1, sizeof(vkvg_text_run_t));
    _font_cache_create_text_run(ctx, text, length, tr);
    return tr;
}
uint32_t vkvg_text_run_get_glyph_count(VkvgText textRun) { return textRun->glyph_count; }
void     vkvg_text_run_get_glyph_position(VkvgText textRun, uint32_t index, vkvg_glyph_info_t *pGlyphInfo) {
    if (index >= textRun->glyph_count) {
        *pGlyphInfo = (vkvg_glyph_info_t){0};
        return;
    }
#if VKVG_USE_HARFBUZZ
    memcpy(pGlyphInfo, &textRun->glyphs[index], sizeof(vkvg_glyph_info_t));
#else
    *pGlyphInfo  = textRun->glyphs[index];
#endif
}
void vkvg_text_run_destroy(VkvgText textRun) {
    _font_cache_destroy_text_run(textRun);
    free(textRun);
}
void vkvg_show_text_run(VkvgContext ctx, VkvgText textRun) {
    if (vkvg_status(ctx))
        return;
    _font_cache_show_text_run(ctx, textRun);
}
void vkvg_text_run_get_extents(VkvgText textRun, vkvg_text_extents_t *extents) { *extents = textRun->extents; }

void vkvg_text_extents(VkvgContext ctx, const char *text, vkvg_text_extents_t *extents) {
    if (vkvg_status(ctx))
        return;
    _font_cache_text_extents(ctx, text, -1, extents);
}
void vkvg_font_extents(VkvgContext ctx, vkvg_font_extents_t *extents) {
    if (vkvg_status(ctx))
        return;
    _font_cache_font_extents(ctx, extents);
}

void vkvg_save(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SAVE);
    LOG(VKVG_LOG_INFO, "SAVE CONTEXT: ctx = %p\n", ctx);

    VkvgDevice           dev = ctx->dev;
    vkvg_context_save_t *sav = (vkvg_context_save_t *)calloc(1, sizeof(vkvg_context_save_t));

    _flush_cmd_buff(ctx);
    if (!_wait_ctx_flush_end(ctx)) {
        free(sav);
        return;
    }

    if (ctx->curClipState == vkvg_clip_state_clip) {
        sav->clippingState = vkvg_clip_state_clip_saved;

        uint8_t curSaveStencil = ctx->curSavBit / 6;

        if (ctx->curSavBit > 0 && ctx->curSavBit % 6 == 0) { // new save/restore stencil image have to be created
            VkhImage *savedStencilsPtr = NULL;
            if (savedStencilsPtr)
                savedStencilsPtr = (VkhImage *)realloc(ctx->savedStencils, curSaveStencil * sizeof(VkhImage));
            else
                savedStencilsPtr = (VkhImage *)malloc(curSaveStencil * sizeof(VkhImage));
            if (savedStencilsPtr == NULL) {
                free(sav);
                ctx->status = VKVG_STATUS_NO_MEMORY;
                return;
            }
            ctx->savedStencils  = savedStencilsPtr;
            VkhImage savStencil = vkh_image_ms_create(
                (VkhDevice)&ctx->dev->vkDev, dev->stencilFormat, dev->samples, ctx->pSurf->width, ctx->pSurf->height,
                VKH_MEMORY_USAGE_GPU_ONLY, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT);
            ctx->savedStencils[curSaveStencil - 1] = savStencil;

            vkh_cmd_begin(ctx->cmd, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
            ctx->cmdStarted = true;

#if defined(DEBUG) && defined(VKVG_DBG_UTILS)
            vkh_cmd_label_start(ctx->cmd, "new save/restore stencil", DBG_LAB_COLOR_SAV);
#endif

            vkh_image_set_layout(ctx->cmd, ctx->pSurf->stencil, dev->stencilAspectFlag,
                                 VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
                                 VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
            vkh_image_set_layout(ctx->cmd, savStencil, dev->stencilAspectFlag, VK_IMAGE_LAYOUT_GENERAL,
                                 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT,
                                 VK_PIPELINE_STAGE_TRANSFER_BIT);

            VkImageCopy cregion = {.srcSubresource = {VK_IMAGE_ASPECT_STENCIL_BIT, 0, 0, 1},
                                   .dstSubresource = {VK_IMAGE_ASPECT_STENCIL_BIT, 0, 0, 1},
                                   .extent         = {ctx->pSurf->width, ctx->pSurf->height, 1}};
            vkCmdCopyImage(ctx->cmd, vkh_image_get_vkimage(ctx->pSurf->stencil), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
                           vkh_image_get_vkimage(savStencil), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &cregion);

            vkh_image_set_layout(ctx->cmd, ctx->pSurf->stencil, dev->stencilAspectFlag,
                                 VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
                                 VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT);

#if defined(DEBUG) && defined(VKVG_DBG_UTILS)
            vkh_cmd_label_end(ctx->cmd);
#endif

            VK_CHECK_RESULT(vkEndCommandBuffer(ctx->cmd));
            _wait_and_submit_cmd(ctx);
        }

        uint8_t curSaveBit = 1 << (ctx->curSavBit % 6 + 2);

        _start_cmd_for_render_pass(ctx);

#if defined(DEBUG) && defined(VKVG_DBG_UTILS)
        vkh_cmd_label_start(ctx->cmd, "save rp", DBG_LAB_COLOR_SAV);
#endif

        CmdBindPipeline(ctx->cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, ctx->dev->pipelineClipping);

        CmdSetStencilReference(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_CLIP_BIT | curSaveBit);
        CmdSetStencilCompareMask(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_CLIP_BIT);
        CmdSetStencilWriteMask(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, curSaveBit);

        _draw_full_screen_quad(ctx, NULL);

        _bind_draw_pipeline(ctx);
        CmdSetStencilCompareMask(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_CLIP_BIT);

#if defined(DEBUG) && defined(VKVG_DBG_UTILS)
        vkh_cmd_label_end(ctx->cmd);
#endif
        ctx->curSavBit++;
    } else if (ctx->curClipState == vkvg_clip_state_none)
        sav->clippingState = (_get_previous_clip_state(ctx) & 0x03);
    else
        sav->clippingState = vkvg_clip_state_clear;

    sav->dashOffset = ctx->dashOffset;
    sav->dashCount  = ctx->dashCount;
    if (ctx->dashCount > 0) {
        sav->dashes = (float *)malloc(sizeof(float) * ctx->dashCount);
        memcpy(sav->dashes, ctx->dashes, sizeof(float) * ctx->dashCount);
    }
    sav->lineWidth   = ctx->lineWidth;
    sav->miterLimit  = ctx->miterLimit;
    sav->curOperator = ctx->curOperator;
    sav->lineCap     = ctx->lineCap;
    sav->lineWidth   = ctx->lineWidth;
    sav->curFillRule = ctx->curFillRule;

    sav->selectedCharSize = ctx->selectedCharSize;
    strcpy(sav->selectedFontName, ctx->selectedFontName);

    sav->currentFont   = ctx->currentFont;
    sav->textDirection = ctx->textDirection;
    sav->pushConsts    = ctx->pushConsts;
    if (ctx->pattern) {
        sav->pattern = ctx->pattern; // TODO:pattern sav must be imutable (copy?)
        vkvg_pattern_reference(ctx->pattern);
    } else
        sav->curColor = ctx->curColor;

    sav->pNext      = ctx->pSavedCtxs;
    ctx->pSavedCtxs = sav;
}
void vkvg_restore(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;

    RECORD(ctx, VKVG_CMD_RESTORE);

    if (ctx->pSavedCtxs == NULL) {
        ctx->status = VKVG_STATUS_INVALID_RESTORE;
        return;
    }

    LOG(VKVG_LOG_INFO, "RESTORE CONTEXT: ctx = %p\n", ctx);

    vkvg_context_save_t *sav = ctx->pSavedCtxs;
    ctx->pSavedCtxs          = sav->pNext;

    _flush_cmd_buff(ctx);
    if (!_wait_ctx_flush_end(ctx))
        return;

    ctx->pushConsts   = sav->pushConsts;
    ctx->pushCstDirty = true;

    if (ctx->curClipState) { //!=none
        if (ctx->curClipState == vkvg_clip_state_clip && sav->clippingState == vkvg_clip_state_clear) {
            _reset_clip(ctx);
        } else {

            uint8_t curSaveBit = 1 << ((ctx->curSavBit - 1) % 6 + 2);

            _start_cmd_for_render_pass(ctx);

#if defined(DEBUG) && defined(VKVG_DBG_UTILS)
            vkh_cmd_label_start(ctx->cmd, "restore rp", DBG_LAB_COLOR_SAV);
#endif

            CmdBindPipeline(ctx->cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, ctx->dev->pipelineClipping);

            CmdSetStencilReference(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_CLIP_BIT | curSaveBit);
            CmdSetStencilCompareMask(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, curSaveBit);
            CmdSetStencilWriteMask(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_CLIP_BIT);

            _draw_full_screen_quad(ctx, NULL);

            _bind_draw_pipeline(ctx);
            CmdSetStencilCompareMask(ctx->cmd, VK_STENCIL_FRONT_AND_BACK, STENCIL_CLIP_BIT);

#if defined(DEBUG) && defined(VKVG_DBG_UTILS)
            vkh_cmd_label_end(ctx->cmd);
#endif

            _flush_cmd_buff(ctx);
            if (!_wait_ctx_flush_end(ctx))
                return;
        }
    }
    if (sav->clippingState == vkvg_clip_state_clip_saved) {
        ctx->curSavBit--;

        uint8_t curSaveStencil = ctx->curSavBit / 6;
        if (ctx->curSavBit > 0 &&
            ctx->curSavBit % 6 ==
                0) { // addtional save/restore stencil image have to be copied back to surf stencil first
            VkhImage savStencil = ctx->savedStencils[curSaveStencil - 1];

            vkh_cmd_begin(ctx->cmd, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
            ctx->cmdStarted = true;

#if defined(DEBUG) && defined(VKVG_DBG_UTILS)
            vkh_cmd_label_start(ctx->cmd, "additional stencil copy while restoring", DBG_LAB_COLOR_SAV);
#endif

            vkh_image_set_layout(ctx->cmd, ctx->pSurf->stencil, ctx->dev->stencilAspectFlag,
                                 VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                 VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
            vkh_image_set_layout(ctx->cmd, savStencil, ctx->dev->stencilAspectFlag,
                                 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
                                 VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);

            VkImageCopy cregion = {.srcSubresource = {VK_IMAGE_ASPECT_STENCIL_BIT, 0, 0, 1},
                                   .dstSubresource = {VK_IMAGE_ASPECT_STENCIL_BIT, 0, 0, 1},
                                   .extent         = {ctx->pSurf->width, ctx->pSurf->height, 1}};
            vkCmdCopyImage(ctx->cmd, vkh_image_get_vkimage(savStencil), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
                           vkh_image_get_vkimage(ctx->pSurf->stencil), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1,
                           &cregion);
            vkh_image_set_layout(ctx->cmd, ctx->pSurf->stencil, ctx->dev->stencilAspectFlag,
                                 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
                                 VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT);

#if defined(DEBUG) && defined(VKVG_DBG_UTILS)
            vkh_cmd_label_end(ctx->cmd);
#endif

            VK_CHECK_RESULT(vkEndCommandBuffer(ctx->cmd));
            _wait_and_submit_cmd(ctx);
            if (!_wait_ctx_flush_end(ctx))
                return;
            vkh_image_destroy(savStencil);
        }
    }

    ctx->curClipState = vkvg_clip_state_none;

    ctx->dashOffset = sav->dashOffset;
    if (ctx->dashCount > 0)
        free(ctx->dashes);
    ctx->dashCount = sav->dashCount;
    if (ctx->dashCount > 0) {
        ctx->dashes = (float *)malloc(sizeof(float) * ctx->dashCount);
        memcpy(ctx->dashes, sav->dashes, sizeof(float) * ctx->dashCount);
    }

    ctx->lineWidth   = sav->lineWidth;
    ctx->miterLimit  = sav->miterLimit;
    ctx->curOperator = sav->curOperator;
    ctx->lineCap     = sav->lineCap;
    ctx->lineJoin    = sav->lineJoint;
    ctx->curFillRule = sav->curFillRule;

    ctx->selectedCharSize = sav->selectedCharSize;
    strcpy(ctx->selectedFontName, sav->selectedFontName);

    ctx->currentFont   = sav->currentFont;
    ctx->textDirection = sav->textDirection;

    if (sav->pattern) {
        if (sav->pattern != ctx->pattern)
            _update_cur_pattern(ctx, sav->pattern);
        else
            vkvg_pattern_destroy(sav->pattern);
    } else {
        ctx->curColor = sav->curColor;
        _update_cur_pattern(ctx, NULL);
    }

    _free_ctx_save(sav);
}

void vkvg_translate(VkvgContext ctx, float dx, float dy) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_TRANSLATE, dx, dy);
    LOG(VKVG_LOG_INFO_CMD, "CMD: translate: %f, %f\n", dx, dy);
    _emit_draw_cmd_undrawn_vertices(ctx);
    vkvg_matrix_translate(&ctx->pushConsts.mat, dx, dy);
    _set_mat_inv_and_vkCmdPush(ctx);
}
void vkvg_scale(VkvgContext ctx, float sx, float sy) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SCALE, sx, sy);
    LOG(VKVG_LOG_INFO_CMD, "CMD: scale: %f, %f\n", sx, sy);
    _emit_draw_cmd_undrawn_vertices(ctx);
    vkvg_matrix_scale(&ctx->pushConsts.mat, sx, sy);
    _set_mat_inv_and_vkCmdPush(ctx);
}
void vkvg_rotate(VkvgContext ctx, float radians) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_ROTATE, radians);
    LOG(VKVG_LOG_INFO_CMD, "CMD: rotate: %f\n", radians);
    _emit_draw_cmd_undrawn_vertices(ctx);
    vkvg_matrix_rotate(&ctx->pushConsts.mat, radians);
    _set_mat_inv_and_vkCmdPush(ctx);
}
void vkvg_transform(VkvgContext ctx, const vkvg_matrix_t *matrix) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_TRANSFORM, matrix);
    LOG(VKVG_LOG_INFO_CMD, "CMD: transform: %f, %f, %f, %f, %f, %f\n", matrix->xx, matrix->yx, matrix->xy, matrix->yy,
        matrix->x0, matrix->y0);
    _emit_draw_cmd_undrawn_vertices(ctx);
    vkvg_matrix_t res;
    vkvg_matrix_multiply(&res, &ctx->pushConsts.mat, matrix);
    ctx->pushConsts.mat = res;
    _set_mat_inv_and_vkCmdPush(ctx);
}
void vkvg_identity_matrix(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_IDENTITY_MATRIX);
    LOG(VKVG_LOG_INFO_CMD, "CMD: identity_matrix:\n");
    _emit_draw_cmd_undrawn_vertices(ctx);
    vkvg_matrix_t im    = VKVG_IDENTITY_MATRIX;
    ctx->pushConsts.mat = im;
    _set_mat_inv_and_vkCmdPush(ctx);
}
void vkvg_set_matrix(VkvgContext ctx, const vkvg_matrix_t *matrix) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_SET_MATRIX, matrix);
    LOG(VKVG_LOG_INFO_CMD, "CMD: set_matrix: %f, %f, %f, %f, %f, %f\n", matrix->xx, matrix->yx, matrix->xy, matrix->yy,
        matrix->x0, matrix->y0);
    _emit_draw_cmd_undrawn_vertices(ctx);
    ctx->pushConsts.mat = (*matrix);
    _set_mat_inv_and_vkCmdPush(ctx);
}
void vkvg_get_matrix(VkvgContext ctx, vkvg_matrix_t *const matrix) {
    if (vkvg_status(ctx) || !matrix)
        return;
    *matrix = ctx->pushConsts.mat;
}

void vkvg_elliptic_arc_to(VkvgContext ctx, float x2, float y2, bool largeArc, bool sweepFlag, float rx, float ry,
                          float phi) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_ELLIPTICAL_ARC_TO, x2, y2, rx, ry, phi, largeArc, sweepFlag);
    LOG(VKVG_LOG_INFO_CMD,
        "\tCMD: elliptic_arc_to: x2:%10.5f y2:%10.5f large:%d sweep:%d rx:%10.5f ry:%10.5f phi:%10.5f \n", x2, y2,
        largeArc, sweepFlag, rx, ry, phi);
    float x1, y1;
    vkvg_get_current_point(ctx, &x1, &y1);
    _elliptic_arc(ctx, x1, y1, x2, y2, largeArc, sweepFlag, rx, ry, phi);
}
void vkvg_rel_elliptic_arc_to(VkvgContext ctx, float x2, float y2, bool largeArc, bool sweepFlag, float rx, float ry,
                              float phi) {
    if (vkvg_status(ctx))
        return;
    RECORD(ctx, VKVG_CMD_REL_ELLIPTICAL_ARC_TO, x2, y2, rx, ry, phi, largeArc, sweepFlag);
    LOG(VKVG_LOG_INFO_CMD,
        "\tCMD: rel_elliptic_arc_to: x2:%10.5f y2:%10.5f large:%d sweep:%d rx:%10.5f ry:%10.5f phi:%10.5f \n", x2, y2,
        largeArc, sweepFlag, rx, ry, phi);

    float x1, y1;
    vkvg_get_current_point(ctx, &x1, &y1);
    _elliptic_arc(ctx, x1, y1, x2 + x1, y2 + y1, largeArc, sweepFlag, rx, ry, phi);
}

void vkvg_ellipse(VkvgContext ctx, float radiusX, float radiusY, float x, float y, float rotationAngle) {
    if (vkvg_status(ctx))
        return;
    LOG(VKVG_LOG_INFO_CMD, "CMD: ellipse:\n");

    float width_two_thirds = radiusX * 4 / 3;

    float dx1 = sinf(rotationAngle) * radiusY;
    float dy1 = cosf(rotationAngle) * radiusY;
    float dx2 = cosf(rotationAngle) * width_two_thirds;
    float dy2 = sinf(rotationAngle) * width_two_thirds;

    float topCenterX = x - dx1;
    float topCenterY = y + dy1;
    float topRightX  = topCenterX + dx2;
    float topRightY  = topCenterY + dy2;
    float topLeftX   = topCenterX - dx2;
    float topLeftY   = topCenterY - dy2;

    float bottomCenterX = x + dx1;
    float bottomCenterY = y - dy1;
    float bottomRightX  = bottomCenterX + dx2;
    float bottomRightY  = bottomCenterY + dy2;
    float bottomLeftX   = bottomCenterX - dx2;
    float bottomLeftY   = bottomCenterY - dy2;

    _finish_path(ctx);
    _add_point(ctx, bottomCenterX, bottomCenterY);

    _curve_to(ctx, bottomRightX, bottomRightY, topRightX, topRightY, topCenterX, topCenterY);
    _curve_to(ctx, topLeftX, topLeftY, bottomLeftX, bottomLeftY, bottomCenterX, bottomCenterY);

    ctx->pathes[ctx->pathPtr] |= PATH_CLOSED_BIT;
    _finish_path(ctx);
}

VkvgSurface vkvg_get_target(VkvgContext ctx) {
    if (vkvg_status(ctx))
        return NULL;
    return ctx->pSurf;
}

const char *vkvg_status_to_string(vkvg_status_t status) {
    switch (status) {
    case VKVG_STATUS_SUCCESS:
        return "no error has occurred";
    /*case VKVG_STATUS_NO_MEMORY:
        return "out of memory";*/
    case VKVG_STATUS_INVALID_RESTORE:
        return "vkvg_restore() without matching vkvg_save()";
    case VKVG_STATUS_NO_CURRENT_POINT:
        return "no current point defined";
    case VKVG_STATUS_INVALID_MATRIX:
        return "invalid matrix (not invertible)";
    case VKVG_STATUS_INVALID_STATUS:
        return "invalid value for an input vkvg_status_t";
    case VKVG_STATUS_INVALID_INDEX:
        return "invalid index passed to getter";
    case VKVG_STATUS_NULL_POINTER:
        return "NULL pointer";
    case VKVG_STATUS_WRITE_ERROR:
        return "error while writing to output stream";
    case VKVG_STATUS_PATTERN_TYPE_MISMATCH:
        return "the pattern type is not appropriate for the operation";
    case VKVG_STATUS_PATTERN_INVALID_GRADIENT:
        return "the stops count is zero";
    case VKVG_STATUS_INVALID_FORMAT:
        return "invalid value for an input vkvg_format_t";
    case VKVG_STATUS_FILE_NOT_FOUND:
        return "file not found";
    case VKVG_STATUS_INVALID_DASH:
        return "invalid value for a dash setting";
    case VKVG_STATUS_INVALID_RECT:
        return "a rectangle has the height or width equal to 0";
    case VKVG_STATUS_TIMEOUT:
        return "waiting for a Vulkan operation to finish resulted in a fence timeout (5 seconds)";
    case VKVG_STATUS_DEVICE_ERROR:
        return "the initialization of the device resulted in an error";
    case VKVG_STATUS_INVALID_IMAGE:
        return "invalid image";
    case VKVG_STATUS_INVALID_SURFACE:
        return "invalid surface";
    case VKVG_STATUS_INVALID_FONT:
        return "unresolved font name";
    default:
        return "<unknown error status>";
    }
}
